CloudFormationとAWS CLIでFargateのBlue/Green Deployment環境を構築する #Fargate
こんにちは、コカコーラ大好きカジです。
以前、VPC構築済みの環境や、同じVPC内に複数のFargateを複数構築するときに使えるCloudFormationテンプレートを作成し公開しました。
上記のCloudFormationテンプレートで構築した環境をBlue/Green Deploymentへ変更しようとした際に、現在マネージメントコンソールから変更できず、解決した方法を記載しておきます。2020年3月時点、上記ブログのCloudFormationから作成したECSのサービス更新から、Blue/Green Deploymentの選択が表示されません。今後変更されると推測しています。(マネージメントコンソールから手動でFargateを構築した場合は影響を受けません。)
CloudFormationのテンプレートを一部追記することで、マネージメントコンソールのECS Serviceの更新からBlue/Green Deploymentの選択できるようになりましたが、全体の手順もまとめてご紹介します。
目次
前提条件
- 上記CloudFormationテンプレートで構築できる準備が整っている必要があります。
- 前回のStackからのUpdate Stackでは名前が重複しエラーとなり、更新できません。そのためStackを一度削除し再作成か、すべて別名での構築となります。
Blue/Green Deploymentとは
Blue/Green Deploymentはデプロイの一つのパターンです。
本番環境内に2つの環境を用意し、トラフィックを片方だけに向けておきます。新しいバージョンのデプロイは本番のトラフィックが流れていない方の環境に行い、その環境上で正しく動作していることを確認したら、トラフィックをその環境に切り替えることで新しいバージョンをリリースします。
今回のFargateに対してのCodeDeployでのBlue/Green Deploymentでは2つのターゲットグループを作成し、ALBからのトラフィックを流すターゲットグループを切り替えることでBlue/Green Deploymentするための変更を行います。
詳細は以下
前回のブログからの変更点(前回のブログを見てくれている人向け)
一番重要なポイントは、ECS Fargateは、以下の2行を追記となります。
# ------------------------------------------------------------# # ECS Service # ------------------------------------------------------------# ECSService: Type: AWS::ECS::Service DependsOn: ALBListener Properties: Cluster: !Ref ECSCluster DesiredCount: !Ref ECSTaskDesiredCount DeploymentController: # 追記 Type: CODE_DEPLOY # 追記
その他変更点
- ALBのターゲットグループとリスナーをそれぞれ1つ追加
- Blue/Green DeploymentのCodeDeployと接続する際に、CodeDeploy用のRoleの指定が必要となるため一緒にCloudFormationで構築
- Blue/Green DeploymentのCodeDeployをAWS CLIで構築 (現時点で、CloudFormation非対応のため)
- マネージメントコンソールのECS Serviceの更新からBlue/Green Deploymentの選択し設定 (現時点で、CloudFormation非対応のため)
作業手順
文末のCloudFormationテンプレートで、ALBとFargateを構築します。(構築方法は前回のブログと同じ)
構築したCloudFormationのリソース情報(赤い四角部分)から文末のJSONファイルを一部修正します。
文末のAWS CLIのスクリプト実行でCodeDeployのBlue/Green Deploymentを構築します。
上記まで完了したら、マネージメントコンソールからECS>作成したServiceの更新から、Blue/Green Deploymentの選択します。
新しいデプロイの強制にチェックを入れて、「次のステップ」をクリック
デプロイメントの設定で、AWS CLIで構築したCodeDeployのアプリケーション名と、デプロイメントグループが選択されていることを確認し「次のステップ」をクリック
次以降は、しばらく「次のステップ」を連打となります。
変更内容を確認し、サービスの更新を押します。
正常に行えると以下のようになり、Blue/Green Deploymentが設定されました。
Blue/Green Deploymentに必要なCode Deploy作成用スクリプトサンプル
AWS CLIがインストール済みの環境で行なってください。
CloudFromationで構築後にCodeDeploy GroupのJSONファイルを修正してから、スクリプトを実行します。
詳細は以下のブログが参考になります。
sample-fargate-deploy-group.json
Create Stackで作成後のリソースを確認し以下のJSONを修正します。ALBのターゲットグループ、リスナーや、Cluster/Service名、CodeDeployのRoleを修正します。
{ "applicationName": "sample-fargate-app", "autoRollbackConfiguration": { "enabled": true, "events": [ "DEPLOYMENT_FAILURE" ] }, "blueGreenDeploymentConfiguration": { "deploymentReadyOption": { "actionOnTimeout": "CONTINUE_DEPLOYMENT", "waitTimeInMinutes": 0 }, "terminateBlueInstancesOnDeploymentSuccess": { "action": "TERMINATE", "terminationWaitTimeInMinutes": 5 } }, "deploymentGroupName": "sample-fargate-deploygroup", "deploymentStyle": { "deploymentOption": "WITH_TRAFFIC_CONTROL", "deploymentType": "BLUE_GREEN" }, "loadBalancerInfo": { "targetGroupPairInfoList": [ { "targetGroups": [ { "name": "sample-fargate-tg1" }, { "name": "sample-fargate-tg2" } ], "prodTrafficRoute": { "listenerArns": [ "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:listener/app/sample-fargate-alb/f064a3708a0a154d/95624f75b7087fb2" ] }, "testTrafficRoute": { "listenerArns": [ "arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:listener/app/sample-fargate-alb/f064a3708a0a154d/64defa76d77a59a5" ] } } ] }, "serviceRoleArn": "arn:aws:iam::xxxxxxxxxxxx:role/sample-fargate-CodeDeployRole", "ecsServices": [ { "serviceName": "sample-fargate-service", "clusterName": "sample-fargate-cluster" } ] }
create-sample-codedeploy.sh
上記JSONファイルを修正してから実行します。
#!/bin/bash aws deploy create-application \ --application-name sample-fargate-app \ --compute-platform ECS \ --region ap-northeast-1 aws deploy create-deployment-group \ --cli-input-json file://sample-fargate-deploy-group.json \ --region ap-northeast-1
スクリプト成功時のログ
$ ./create-sample-codedeploy.sh { "applicationId": "ebbd58ba-a9e2-4ec4-94dc-xxxxxxxxxxxx" } { "deploymentGroupId": "c5df86a3-75dd-4926-9ba3-xxxxxxxxxxxx" }
CloudFormationテンプレート
構築方法は前回のブログと変わりません。
AWSTemplateFormatVersion: "2010-09-09" Description: Fargate and ALB Create Metadata: "AWS::CloudFormation::Interface": ParameterGroups: - Label: default: "Project Name Prefix" Parameters: - ProjectName - Label: default: "InternetALB Configuration" Parameters: - InternetALBName - TargetGroupName1 - TargetGroupName2 - Label: default: "Fargate for ECS Configuration" Parameters: - ECSClusterName - ECSTaskName - ECSTaskCPUUnit - ECSTaskMemory - ECSContainerName - ECSImageName - ECSServiceName - ECSTaskDesiredCount - Label: default: "Netowork Configuration" Parameters: - VpcId - ALBSecurityGroupId - ALBSubnetId1 - ALBSubnetId2 - ECSSecurityGroupId - ECSSubnetId1 - ECSSubnetId2 - Label: default: "Scaling Configuration" Parameters: - ServiceScaleEvaluationPeriods - ServiceCpuScaleOutThreshold - ServiceCpuScaleInThreshold - TaskMinContainerCount - TaskMaxContainerCount ParameterLabels: InternetALBName: default: "InternetALBName" TargetGroupName1: default: "TargetGroupName1" TargetGroupName2: default: "TargetGroupName2" ECSClusterName: default: "ECSClusterName" ECSTaskName: default: "ECSTaskName" ECSTaskCPUUnit: default: "ECSTaskCPUUnit" ECSTaskMemory: default: "ECSTaskMemory" ECSContainerName: default: "ECSContainerName" ECSImageName: default: "ECSImageName" ECSServiceName: default: "ECSServiceName" ECSTaskDesiredCount: default: "ECSTaskDesiredCount" # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: ProjectName: Default: sample-fargate Type: String #VPCID VpcId: Description: "VPC ID" Type: AWS::EC2::VPC::Id #ALBSecurity Group ALBSecurityGroupId: Type: AWS::EC2::SecurityGroup::Id #ALBSubnet1 ALBSubnetId1: Description: "ALB Subnet 1st" Type: AWS::EC2::Subnet::Id #ALBSubnet2 ALBSubnetId2: Description: "ALB Subnet 2st" Type: AWS::EC2::Subnet::Id #ECSSecurity Group ECSSecurityGroupId: Type: AWS::EC2::SecurityGroup::Id #ECSSubnet1 ECSSubnetId1: Description: "ECS Subnet 1st" Type: AWS::EC2::Subnet::Id #ECSSubnet2 ECSSubnetId2: Description: "ECS Subnet 2st" Type: AWS::EC2::Subnet::Id #InternetALB InternetALBName: Type: String Default: "alb" #TargetGroupName1 TargetGroupName1: Type: String Default: "tg1" #TargetGroupName2 TargetGroupName2: Type: String Default: "tg2" #ECSClusterName ECSClusterName: Type: String Default: "cluster" #ECSTaskName ECSTaskName: Type: String Default: "task" #ECSTaskCPUUnit ECSTaskCPUUnit: AllowedValues: [256, 512, 1024, 2048, 4096] Type: String Default: "256" #ECSTaskMemory ECSTaskMemory: AllowedValues: [256, 512, 1024, 2048, 4096] Type: String Default: "512" #ECSContainerName ECSContainerName: Type: String Default: "container" #ECSImageName ECSImageName: Type: String Default: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/kaji-test-ecr:latest" #ECSServiceName ECSServiceName: Type: String Default: "service" #ECSTaskDesiredCount ECSTaskDesiredCount: Type: Number Default: 1 # Scaling params ServiceScaleEvaluationPeriods: Description: The number of periods over which data is compared to the specified threshold Type: Number Default: 2 MinValue: 2 ServiceCpuScaleOutThreshold: Type: Number Description: Average CPU value to trigger auto scaling out Default: 50 MinValue: 0 MaxValue: 100 ConstraintDescription: Value must be between 0 and 100 ServiceCpuScaleInThreshold: Type: Number Description: Average CPU value to trigger auto scaling in Default: 25 MinValue: 0 MaxValue: 100 ConstraintDescription: Value must be between 0 and 100 TaskMinContainerCount: Type: Number Description: Minimum number of containers to run for the service Default: 1 MinValue: 1 ConstraintDescription: Value must be at least one TaskMaxContainerCount: Type: Number Description: Maximum number of containers to run for the service when auto scaling out Default: 2 MinValue: 1 ConstraintDescription: Value must be at least one Resources: # ------------------------------------------------------------# # Target Group # ------------------------------------------------------------# TargetGroup1: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: VpcId: !Ref VpcId Name: !Sub "${ProjectName}-${TargetGroupName1}" Protocol: HTTP Port: 80 TargetType: ip TargetGroup2: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: VpcId: !Ref VpcId Name: !Sub "${ProjectName}-${TargetGroupName2}" Protocol: HTTP Port: 80 TargetType: ip # ------------------------------------------------------------# # Internet ALB # ------------------------------------------------------------# InternetALB: Type: "AWS::ElasticLoadBalancingV2::LoadBalancer" Properties: Name: !Sub "${ProjectName}-${InternetALBName}" Tags: - Key: Name Value: !Sub "${ProjectName}-${InternetALBName}" Scheme: "internet-facing" LoadBalancerAttributes: - Key: "deletion_protection.enabled" Value: false - Key: "idle_timeout.timeout_seconds" Value: 60 - Key: "access_logs.s3.enabled" Value: true - Key: "access_logs.s3.bucket" Value: !Sub "alb-log-${AWS::AccountId}" SecurityGroups: - !Ref ALBSecurityGroupId Subnets: - !Ref ALBSubnetId1 - !Ref ALBSubnetId2 ALBListener1: Type: "AWS::ElasticLoadBalancingV2::Listener" Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup1 Type: forward LoadBalancerArn: !Ref InternetALB Port: 80 Protocol: HTTP ALBListener2: Type: "AWS::ElasticLoadBalancingV2::Listener" Properties: DefaultActions: - TargetGroupArn: !Ref TargetGroup2 Type: forward LoadBalancerArn: !Ref InternetALB Port: 8080 Protocol: HTTP # ------------------------------------------------------------# # ECS Cluster # ------------------------------------------------------------# ECSCluster: Type: "AWS::ECS::Cluster" Properties: ClusterName: !Sub "${ProjectName}-${ECSClusterName}" # ------------------------------------------------------------# # ECS LogGroup # ------------------------------------------------------------# ECSLogGroup: Type: "AWS::Logs::LogGroup" Properties: LogGroupName: !Sub "/ecs/logs/${ProjectName}-ecs-group" # ------------------------------------------------------------# # ECS Task Execution Role # ------------------------------------------------------------# ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${ProjectName}-ECSTaskExecutionRolePolicy" Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: ecs-tasks.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy # ------------------------------------------------------------# # ECS TaskDefinition # ------------------------------------------------------------# ECSTaskDefinition: Type: "AWS::ECS::TaskDefinition" Properties: Cpu: !Ref ECSTaskCPUUnit ExecutionRoleArn: !Ref ECSTaskExecutionRole Family: !Sub "${ProjectName}-${ECSTaskName}" Memory: !Ref ECSTaskMemory NetworkMode: awsvpc RequiresCompatibilities: - FARGATE #ContainerDefinitions ContainerDefinitions: - Name: !Sub "${ProjectName}-${ECSContainerName}" Image: !Ref ECSImageName LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref ECSLogGroup awslogs-region: !Ref "AWS::Region" awslogs-stream-prefix: !Ref ProjectName MemoryReservation: 128 PortMappings: - HostPort: 80 Protocol: tcp ContainerPort: 80 # ------------------------------------------------------------# # ECS Service # ------------------------------------------------------------# ECSService: Type: AWS::ECS::Service DependsOn: ALBListener1 Properties: Cluster: !Ref ECSCluster DesiredCount: !Ref ECSTaskDesiredCount DeploymentController: Type: CODE_DEPLOY LaunchType: FARGATE LoadBalancers: - TargetGroupArn: !Ref TargetGroup1 ContainerPort: 80 ContainerName: !Sub "${ProjectName}-${ECSContainerName}" NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !Ref ECSSecurityGroupId Subnets: - !Ref ECSSubnetId1 - !Ref ECSSubnetId2 ServiceName: !Sub "${ProjectName}-${ECSServiceName}" TaskDefinition: !Ref ECSTaskDefinition # ------------------------------------------------------------# # Auto Scaling Service # ------------------------------------------------------------# ServiceAutoScalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: application-autoscaling.amazonaws.com Action: sts:AssumeRole Path: / Policies: - PolicyName: !Sub "${ProjectName}-${ECSContainerName}-autoscaling" PolicyDocument: Statement: - Effect: Allow Action: - application-autoscaling:* - cloudwatch:DescribeAlarms - cloudwatch:PutMetricAlarm - ecs:DescribeServices - ecs:UpdateService Resource: "*" ServiceScalingTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget Properties: MinCapacity: !Ref TaskMinContainerCount MaxCapacity: !Ref TaskMaxContainerCount ResourceId: !Sub - service/${EcsClusterName}/${EcsDefaultServiceName} - EcsClusterName: !Ref ECSCluster EcsDefaultServiceName: !Sub "${ProjectName}-${ECSServiceName}" RoleARN: !GetAtt ServiceAutoScalingRole.Arn ScalableDimension: ecs:service:DesiredCount ServiceNamespace: ecs DependsOn: - ECSService - ServiceAutoScalingRole ServiceScaleOutPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub "${ProjectName}-${ECSServiceName}-ScaleOutPolicy" PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalingTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 60 MetricAggregationType: Average StepAdjustments: - ScalingAdjustment: 1 MetricIntervalLowerBound: 0 DependsOn: ServiceScalingTarget ServiceScaleInPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub "${ProjectName}-${ECSServiceName}-ScaleInPolicy" PolicyType: StepScaling ScalingTargetId: !Ref ServiceScalingTarget StepScalingPolicyConfiguration: AdjustmentType: ChangeInCapacity Cooldown: 60 MetricAggregationType: Average StepAdjustments: - ScalingAdjustment: -1 MetricIntervalUpperBound: 0 DependsOn: ServiceScalingTarget ServiceScaleOutAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub "${ProjectName}-${ECSServiceName}-ScaleOutAlarm" EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods Statistic: Average TreatMissingData: notBreaching Threshold: !Ref ServiceCpuScaleOutThreshold AlarmDescription: Alarm to add capacity if CPU is high Period: 60 AlarmActions: - !Ref ServiceScaleOutPolicy Namespace: AWS/ECS Dimensions: - Name: ClusterName Value: !Ref ECSCluster - Name: ServiceName Value: !Sub "${ProjectName}-${ECSServiceName}" ComparisonOperator: GreaterThanThreshold MetricName: CPUUtilization DependsOn: - ECSService - ServiceScaleOutPolicy ServiceScaleInAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: !Sub "${ProjectName}-${ECSServiceName}-ScaleInAlarm" EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods Statistic: Average TreatMissingData: notBreaching Threshold: !Ref ServiceCpuScaleInThreshold AlarmDescription: Alarm to reduce capacity if container CPU is low Period: 300 AlarmActions: - !Ref ServiceScaleInPolicy Namespace: AWS/ECS Dimensions: - Name: ClusterName Value: !Ref ECSCluster - Name: ServiceName Value: !Sub "${ProjectName}-${ECSServiceName}" ComparisonOperator: LessThanThreshold MetricName: CPUUtilization DependsOn: - ECSService - ServiceScaleInPolicy # ------------------------------------------------------------# # BlueGreen CodeDeploy Role # ------------------------------------------------------------# CodeDeployRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${ProjectName}-CodeDeployRole" Path: / AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: codedeploy.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
感想
自分でハマった点を解決したメモを公開しました。どなたかのお役に立てれば光栄です。 また、今後、CloudFormationでFargateとCodeDeployのBlue/Green Deploymentもまとめて構築できるようになり、サンプルテンプレートが公開されることを期待しております。